﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Windows;
using System.Windows.Resources;
using System.Xml;
using System.Diagnostics;
using System.ServiceModel;

namespace NASA.BeAMartian.Plugin
{
    //public delegate void XapDownloadProgressChangedEvent(object sender, XapProgressEventArgs e);
    //public delegate void XapDownloadCompletedEvent(object sender, XapDownloadCompletedEventArgs e);


    /// <summary>
    /// This class loads multiple xaps and caches them allowing objects to be created from their content.
    /// </summary>
    public class XapLoader
    {
        //public event XapDownloadProgressChangedEvent DownloadProgressChanged;
        //public event XapDownloadCompletedEvent DownloadCompleted;

        public event EventHandler<XapProgressEventArgs> DownloadProgressChanged;
        public event EventHandler<XapDownloadCompletedEventArgs> DownloadCompleted;

        private Dictionary<Uri, int> inTransit = new Dictionary<Uri, int>();
        private Dictionary<Uri, Assembly> loadedAssemblies = new Dictionary<Uri, Assembly>();
        private List<Assembly> assembliesList = new List<Assembly>();
        private int lastIndex = 0;

        public XapLoader()
        {
            this.MaxAttempts = 1;
        }

        public int MaxAttempts { get; set; }

        public void LoadXapAsync(Uri xapUri)
        {
            this.LoadXapAsync(xapUri, null);
        }

        public void LoadXapAsync(Uri xapUri, object userToken)
        {
            if (loadedAssemblies.ContainsKey(xapUri) == false)
            {
                if (inTransit.ContainsKey(xapUri) == false)
                {
                    Debug.WriteLine("[{2}] Attempt #{0} to load url: '{1}'.", 1, xapUri, DateTime.Now);
                    inTransit[xapUri] = lastIndex++;
                    WebClient webClient = new WebClient();
                    webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
                    webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(webClient_DownloadProgressChanged);
                    webClient.OpenReadAsync(xapUri, new XapLoaderToken(xapUri, userToken));
                }
            }
            else
            {
                this.OnDownloadCompleted(null, new XapLoaderToken(xapUri, userToken), false, null);
            }
        }

        // Work in progress. The idea is to list out all the dependencies. If those dependencies are not already
        // loaded, then load them.
        public void LoadDependencyNodeAsync(XapDependencyNode dependencyNode)
        {
            foreach (XapDependencyNode node in dependencyNode.Dependencies)
            {
                if(!node.IsLoading && !node.IsLoaded)
                {
                    this.LoadDependencyNodeAsync(node);
                    return;
                }
            }

            if (!dependencyNode.IsLoading && !dependencyNode.IsLoaded)
            {

            }
        }

        public Object CreateInstance(string typeName, params object[] args)
        {
            Object ret = null;
            Type type = null;

            foreach (Assembly assembly in assembliesList)
            {
                type = assembly.GetType(typeName, false);
                if (type != null)
                {
                    break;
                }
            }

            if (type != null)
            {
                ret = Activator.CreateInstance(type, args);
            }

            return ret;
        }

        public Object CreateInstance(Uri xapUri, string typeName, params object [] args)
        {
            Object ret = null;

            Type type = null;
            Assembly assembly = null;

            if (loadedAssemblies.TryGetValue(xapUri, out assembly))
            {
                type = assembly.GetType(typeName, false);
            }

            if (type != null)
            {
                ret = Activator.CreateInstance(type, args);
            }

            return ret;
        }

        protected void OnDownloadProgressChanged(Uri xapUri, DownloadProgressChangedEventArgs e, object userToken)
        {
            if (this.DownloadProgressChanged != null)
            {
                this.DownloadProgressChanged(this, new XapProgressEventArgs(xapUri, e, e.UserState));
            }
        }

        private void webClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            XapLoaderToken loaderToken = (XapLoaderToken)e.UserState;
            OnDownloadProgressChanged(loaderToken.Uri, e, loaderToken.UserToken);
        }

        internal void OnDownloadCompleted(WebClient webClient, XapLoaderToken token, bool canceled, Exception ex)
        {
            this.inTransit.Remove(token.Uri);

            if (webClient != null)
            {
                webClient.OpenReadCompleted -= new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
                webClient.DownloadProgressChanged -= new DownloadProgressChangedEventHandler(webClient_DownloadProgressChanged);
            }

            if (this.DownloadCompleted != null)
            {
                this.DownloadCompleted(this, new XapDownloadCompletedEventArgs(token.Uri, token.UserToken, canceled, ex));
            }
        }

        private void webClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            XapLoaderToken loaderToken = (XapLoaderToken)e.UserState;
            WebClient webClient = (WebClient)sender;

            // On error, retry if there was a timeout or comm problem.
            if (e.Error != null)
            {
                if (e.Error is TimeoutException || e.Error is CommunicationException)
                {
                    loaderToken.Attempts++;

                    if (loaderToken.Attempts >= this.MaxAttempts) // Final timeout after n attempts
                    {
                        Debug.WriteLine("[{2}], Final exception '{3}' loading url '{0}' after {1} attempts.", loaderToken.Uri, loaderToken.Attempts, DateTime.Now, e.GetType().Name);
                        OnDownloadCompleted(webClient, loaderToken, e.Cancelled, e.Error);
                    }
                    else
                    {
                        // Local timeout
                        Debug.WriteLine("[{2}] Timeout occured loading url '{0}' (attempt #{1}).", loaderToken.Uri, loaderToken.Attempts, DateTime.Now);
                        Debug.WriteLine("[{2}] Attempt #{0} to load url: '{1}'.", loaderToken.Attempts + 1, loaderToken.Uri, DateTime.Now);
                        webClient.OpenReadAsync(loaderToken.Uri, loaderToken);
                    }

                    return;
                }
            }
            else if(e.Cancelled == false)
            {
                try
                {
                    int number = inTransit[loaderToken.Uri];

                    Assembly assembly = this.LoadAssembly(e.Result);

                    loadedAssemblies[loaderToken.Uri] = assembly;

                    if (number > assembliesList.Count - 1)
                    {
                        for (int i = assembliesList.Count; i < number; i++)
                        {
                            assembliesList.Add(null);
                        }
                        assembliesList.Add(assembly);
                    }
                    else
                    {
                        assembliesList[number] = assembly;
                    }
                }
                catch (Exception ex)
                {
                    OnDownloadCompleted(webClient, loaderToken, e.Cancelled, ex);
                    return;
                }
            }

            OnDownloadCompleted(webClient, loaderToken, e.Cancelled, e.Error);
        }

        protected Assembly LoadAssembly(Stream package)
        {
            // Extract the AppManifest from the XAP package
            string appManifestString = new StreamReader(
                Application.GetResourceStream(
                    new StreamResourceInfo(package, null),
                    new Uri("AppManifest.xaml", UriKind.Relative)
                    ).Stream).ReadToEnd();

            // Use the XamlReader to parse the AppManifest into managed objects
            //Deployment deployment = (Deployment)XamlReader.Load(appManifestString);

            XmlReader reader = XmlReader.Create(new StringReader(appManifestString));
            string entryPoint = null;

            // Keep track of the main assembly,
            // we'll assume that the element to create is located here.
            Assembly mainAssembly = null;

            while (reader.Read())
            {
                if (reader.IsStartElement("Deployment"))
                {
                    if (reader.MoveToAttribute("EntryPointAssembly"))
                    {
                        reader.ReadAttributeValue();
                        entryPoint = reader.Value;
                    }
                }

                if (reader.IsStartElement("AssemblyPart"))
                {
                    reader.MoveToAttribute("Source");
                    reader.ReadAttributeValue();
                    string source = reader.Value;

                    StreamResourceInfo streamInfo = Application.GetResourceStream(
                        new StreamResourceInfo(package, "application/binary"),new Uri(source, UriKind.Relative));
                    AssemblyPart assemblyPart = new AssemblyPart();
                    Assembly assembly = null;////////////////////////////////////////////////// assemblyPart.Load(streamInfo.Stream);

                    if (assembly.FullName.Split(',')[0] == entryPoint)
                        mainAssembly = assembly;
                }
            }

            return mainAssembly;
        }
    }
}

